package org.hxzon.configdesigner.web.components;

import java.util.Iterator;
import java.util.List;
import java.util.Stack;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.Block;
import org.apache.tapestry5.ComponentAction;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.EventConstants;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.ValueEncoder;
import org.apache.tapestry5.annotations.AfterRender;
import org.apache.tapestry5.annotations.AfterRenderBody;
import org.apache.tapestry5.annotations.BeforeRenderBody;
import org.apache.tapestry5.annotations.BeginRender;
import org.apache.tapestry5.annotations.CleanupRender;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Events;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.SetupRender;
import org.apache.tapestry5.annotations.SupportsInformalParameters;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
import org.apache.tapestry5.services.ComponentDefaultProvider;
import org.apache.tapestry5.services.FormSupport;
import org.apache.tapestry5.services.Heartbeat;
import org.hxzon.configdesigner.core.CfgInfo;
import org.hxzon.configdesigner.core.CfgValue;

@SupportsInformalParameters
@Events(EventConstants.SYNCHRONIZE_VALUES)
public class CfgValueLoop {

    /**
     * Used in both volatile and non-volatile mode to end the current heartbeat (started by either ADVANCE_VOLATILE or
     * one of the RestoreState commands). Also increments the index.
     */
    private static final ComponentAction<CfgValueLoop> END_HEARTBEAT = new ComponentAction<CfgValueLoop>() {
        private static final long serialVersionUID = -977168791667037377L;

        public void execute(CfgValueLoop component) {
            component.endHeartbeat();
        }

        @Override
        public String toString() {
            return "CfgRender.EndHeartbeat";
        }
    };

    /**
     * Restores the value using a stored primary key via {@link ValueEncoder#toValue(String)}.
     */
    @SuppressWarnings("serial")
    static class RestoreStateFromStoredClientValue implements ComponentAction<CfgValueLoop> {
        private final String clientValue;

        public RestoreStateFromStoredClientValue(final String clientValue) {
            this.clientValue = clientValue;
        }

        public void execute(CfgValueLoop component) {
            component.restoreStateFromStoredClientValue(clientValue);
        }

        @Override
        public String toString() {
            return String.format("CfgRender.RestoreStateFromStoredClientValue[%s]", clientValue);
        }
    }

    /**
     * Start of processing event that allows the CfgRender to set up internal bookeeping, to track which values have come up
     * in the form submission.
     */
    @SuppressWarnings("serial")
    static final ComponentAction<CfgValueLoop> PREPARE_FOR_SUBMISSION = new ComponentAction<CfgValueLoop>() {
        public void execute(CfgValueLoop component) {
            component.prepareForSubmission();
        }

        @Override
        public String toString() {
            return "CfgRender.PrepareForSubmission";
        }
    };

    @SuppressWarnings("serial")
    static final ComponentAction<CfgValueLoop> NOTIFY_CONTAINER = new ComponentAction<CfgValueLoop>() {
        public void execute(CfgValueLoop component) {
            component.notifyContainer();
        }

        @Override
        public String toString() {
            return "CfgRender.NotifyContainer";
        }
    };

    /**
     * Defines the collection of values for the CfgRender to iterate over. If not specified, defaults to a property of the
     * container whose name matches the Loop cmponent's id.
     */
    @Parameter(required = true, principal = true, autoconnect = true)
    private CfgValue cfg;

    /**
     * A ValueEncoder used to convert server-side objects (provided by the
     * "value" parameter) into unique client-side strings (typically IDs) and
     * back. In general, when using a non-volatile CfgRender in a Form, you should
     * either provide a ValueEncoder with the encoder parameter or use a "value"
     * type for which Tapestry is configured to provide a ValueEncoder
     * automatically. Otherwise Tapestry must fall back to using the plain
     * index of each loop iteration, rather than the ValueEncoder-provided
     * unique ID, for recording state into the form.
     */
    @Parameter
    private ValueEncoder<CfgValue> encoder;

    @Environmental(false)
    private FormSupport formSupport;

    /**
     * The element to render. If not null, then the loop will render the indicated element around its body (on each pass
     * through the loop). The default is derived from the component template.
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private String element;

    /**
     * The current value, set before the component renders its body.
     */
    @Parameter(principal = true)
    private CfgValue value;

    /**
     * A Block to render instead of the loop when the source is empty. The default is to render nothing.
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private Block empty;

    private Stack<Iterator<CfgValue>> iteratorStack;
    private CfgValue nextValue;

    @Environmental
    private Heartbeat heartbeat;

    @Inject
    private ComponentResources resources;

    @Inject
    private ComponentDefaultProvider defaultProvider;

    private Block cleanupBlock;

    /**
     * Objects that have been recovered via {@link org.apache.tapestry5.ValueEncoder#toValue(String)} during the
     * processing of the loop. These are sent to the container via an event.
     */
    private List<CfgValue> synchonizedValues;

    String defaultElement() {
        return resources.getElementName();
    }

    @SuppressWarnings("unchecked")
    ValueEncoder<CfgValue> defaultEncoder() {
        return defaultProvider.defaultValueEncoder("value", resources);
    }

    @SetupRender
    boolean setup() {
        iteratorStack = new Stack<Iterator<CfgValue>>();
        nextValue = cfg;

        formSupport.store(this, PREPARE_FOR_SUBMISSION);
        // Only render the body if there is something to iterate over

        boolean hasContent = nextValue != null;

        cleanupBlock = hasContent ? null : empty;

        // Jump directly to cleanupRender if there is no content

        return hasContent;
    }

    /**
     * Returns the empty block, or null, after the render has finished. It will only be the empty block (which itself
     * may be null) if the source was null or empty.
     */
    @CleanupRender
    Block cleanupRender(MarkupWriter writer) {
        formSupport.store(this, NOTIFY_CONTAINER);
        return cleanupBlock;
    }

    private boolean isEmbedCombo(CfgValue cfgValue) {
        if (cfgValue == cfg || //
                (cfgValue.getCfgInfo().getType() > CfgInfo.Type_Combo //
                && cfgValue.getCfgInfo().isEmbed())) {
            return true;
        }
        return false;
    }

    @BeforeRenderBody
    void beforeRenderBody(MarkupWriter writer) {
        if (isEmbedCombo(value)) {
            writer.element("div", "class", "embedBlock");
        }
    }

    @AfterRenderBody
    void afterRenderBody(MarkupWriter writer) {
        {
            if (isEmbedCombo(value)) {
                iteratorStack.push(value.getValues().iterator());
            }
            nextValue = null;
            while (nextValue == null && !iteratorStack.isEmpty()) {
                Iterator<CfgValue> iter = iteratorStack.peek();
                if (!iter.hasNext()) {
                    writer.end();
                    iteratorStack.pop();
                } else {
                    nextValue = iter.next();
                }
            }
        }
    }

    /**
     * Begins a new heartbeat.
     */
    @BeginRender
    void begin(MarkupWriter writer) {
        value = nextValue;//will not null

        String clientValue = encoder.toClient(value);

        formSupport.store(this, new RestoreStateFromStoredClientValue(clientValue));

        startHeartbeat();

        if (element != null) {
            writer.element(element);
            resources.renderInformalParameters(writer);
        }
    }

    private void startHeartbeat() {
        heartbeat.begin();
    }

    /**
     * Ends the current heartbeat.
     */
    @AfterRender
    Boolean after(MarkupWriter writer) {
        if (element != null) {
            writer.end();
        }
        endHeartbeat();

        formSupport.store(this, END_HEARTBEAT);

        return nextValue != null ? false : null;
    }

    private void endHeartbeat() {
        heartbeat.end();
    }

    /**
     * Restores state previously stored by the Loop into a Form.
     */
    private void restoreState(CfgValue storedValue) {
        value = storedValue;

        startHeartbeat();
    }

    /**
     * Restores state previously encoded by the Loop and stored into the Form.
     */
    private void restoreStateFromStoredClientValue(String clientValue) {
        // We assume that if an encoder is available when we rendered, that one will be available
        // when the form is submitted.

        CfgValue restoredValue = encoder.toValue(clientValue);

        restoreState(restoredValue);

        synchonizedValues.add(restoredValue);
    }

    private void prepareForSubmission() {
        synchonizedValues = CollectionFactory.newList();
    }

    private void notifyContainer() {
        Object[] values = synchonizedValues.toArray();

        resources.triggerEvent(EventConstants.SYNCHRONIZE_VALUES, values, null);
    }

}
